{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a38273b5-382f-4377-b718-ee8860e40880",
   "metadata": {},
   "source": [
    "# 词嵌入（word2vec）\n",
    ":label:`sec_word2vec`\n",
    "\n",
    "自然语言是用来表达人脑思维的复杂系统。\n",
    "在这个系统中，词是意义的基本单元。顾名思义，\n",
    "*词向量*是用于表示单词意义的向量，\n",
    "并且还可以被认为是单词的特征向量或表示。\n",
    "将单词映射到实向量的技术称为*词嵌入*。\n",
    "近年来，词嵌入逐渐成为自然语言处理的基础知识。\n",
    "\n",
    "## 为何独热向量是一个糟糕的选择\n",
    "\n",
    "在 :numref:`sec_rnn_scratch`中，我们使用独热向量来表示词（字符就是单词）。假设词典中不同词的数量（词典大小）为$N$，每个词对应一个从$0$到$N−1$的不同整数（索引）。为了得到索引为$i$的任意词的独热向量表示，我们创建了一个全为0的长度为$N$的向量，并将位置$i$的元素设置为1。这样，每个词都被表示为一个长度为$N$的向量，可以直接由神经网络使用。\n",
    "\n",
    "虽然独热向量很容易构建，但它们通常不是一个好的选择。一个主要原因是独热向量不能准确表达不同词之间的相似度，比如我们经常使用的“余弦相似度”。对于向量$\\mathbf{x}, \\mathbf{y} \\in \\mathbb{R}^d$，它们的余弦相似度是它们之间角度的余弦：\n",
    "\n",
    "$$\\frac{\\mathbf{x}^\\top \\mathbf{y}}{\\|\\mathbf{x}\\| \\|\\mathbf{y}\\|} \\in [-1, 1].$$\n",
    "\n",
    "由于任意两个不同词的独热向量之间的余弦相似度为0，所以独热向量不能编码词之间的相似性。\n",
    "\n",
    "## 自监督的word2vec\n",
    "\n",
    "[word2vec](https://code.google.com/archive/p/word2vec/)工具是为了解决上述问题而提出的。它将每个词映射到一个固定长度的向量，这些向量能更好地表达不同词之间的相似性和类比关系。word2vec工具包含两个模型，即*跳元模型*（skip-gram） :cite:`Mikolov.Sutskever.Chen.ea.2013`和*连续词袋*（CBOW） :cite:`Mikolov.Chen.Corrado.ea.2013`。对于在语义上有意义的表示，它们的训练依赖于条件概率，条件概率可以被看作使用语料库中一些词来预测另一些单词。由于是不带标签的数据，因此跳元模型和连续词袋都是自监督模型。\n",
    "\n",
    "下面，我们将介绍这两种模式及其训练方法。\n",
    "\n",
    "## 跳元模型（Skip-Gram）\n",
    ":label:`subsec_skip-gram`\n",
    "\n",
    "跳元模型假设一个词可以用来在文本序列中生成其周围的单词。以文本序列“the”“man”“loves”“his”“son”为例。假设*中心词*选择“loves”，并将上下文窗口设置为2，如图 :numref:`fig_skip_gram`所示，给定中心词“loves”，跳元模型考虑生成*上下文词*“the”“man”“him”“son”的条件概率：\n",
    "\n",
    "$$P(\\textrm{\"the\"},\\textrm{\"man\"},\\textrm{\"his\"},\\textrm{\"son\"}\\mid\\textrm{\"loves\"}).$$\n",
    "\n",
    "假设上下文词是在给定中心词的情况下独立生成的（即条件独立性）。在这种情况下，上述条件概率可以重写为：\n",
    "\n",
    "$$P(\\textrm{\"the\"}\\mid\\textrm{\"loves\"})\\cdot P(\\textrm{\"man\"}\\mid\\textrm{\"loves\"})\\cdot P(\\textrm{\"his\"}\\mid\\textrm{\"loves\"})\\cdot P(\\textrm{\"son\"}\\mid\\textrm{\"loves\"}).$$\n",
    "\n",
    "![跳元模型考虑了在给定中心词的情况下生成周围上下文词的条件概率](../img/skip-gram.svg)\n",
    ":label:`fig_skip_gram`\n",
    "\n",
    "在跳元模型中，每个词都有两个$d$维向量表示，用于计算条件概率。更具体地说，对于词典中索引为$i$的任何词，分别用$\\mathbf{v}_i\\in\\mathbb{R}^d$和$\\mathbf{u}_i\\in\\mathbb{R}^d$表示其用作*中心词*和*上下文词*时的两个向量。给定中心词$w_c$（词典中的索引$c$），生成任何上下文词$w_o$（词典中的索引$o$）的条件概率可以通过对向量点积的softmax操作来建模：\n",
    "\n",
    "$$P(w_o \\mid w_c) = \\frac{\\text{exp}(\\mathbf{u}_o^\\top \\mathbf{v}_c)}{ \\sum_{i \\in \\mathcal{V}} \\text{exp}(\\mathbf{u}_i^\\top \\mathbf{v}_c)},$$\n",
    ":eqlabel:`eq_skip-gram-softmax`\n",
    "\n",
    "其中词表索引集$\\mathcal{V} = \\{0, 1, \\ldots, |\\mathcal{V}|-1\\}$。给定长度为$T$的文本序列，其中时间步$t$处的词表示为$w^{(t)}$。假设上下文词是在给定任何中心词的情况下独立生成的。对于上下文窗口$m$，跳元模型的似然函数是在给定任何中心词的情况下生成所有上下文词的概率：\n",
    "\n",
    "$$ \\prod_{t=1}^{T} \\prod_{-m \\leq j \\leq m,\\ j \\neq 0} P(w^{(t+j)} \\mid w^{(t)}),$$\n",
    "\n",
    "其中可以省略小于$1$或大于$T$的任何时间步。\n",
    "\n",
    "### 训练\n",
    "\n",
    "跳元模型参数是词表中每个词的中心词向量和上下文词向量。在训练中，我们通过最大化似然函数（即极大似然估计）来学习模型参数。这相当于最小化以下损失函数：\n",
    "\n",
    "$$ - \\sum_{t=1}^{T} \\sum_{-m \\leq j \\leq m,\\ j \\neq 0} \\text{log}\\, P(w^{(t+j)} \\mid w^{(t)}).$$\n",
    "\n",
    "当使用随机梯度下降来最小化损失时，在每次迭代中可以随机抽样一个较短的子序列来计算该子序列的（随机）梯度，以更新模型参数。为了计算该（随机）梯度，我们需要获得对数条件概率关于中心词向量和上下文词向量的梯度。通常，根据 :eqref:`eq_skip-gram-softmax`，涉及中心词$w_c$和上下文词$w_o$的对数条件概率为：\n",
    "\n",
    "$$\\log P(w_o \\mid w_c) =\\mathbf{u}_o^\\top \\mathbf{v}_c - \\log\\left(\\sum_{i \\in \\mathcal{V}} \\text{exp}(\\mathbf{u}_i^\\top \\mathbf{v}_c)\\right).$$\n",
    ":eqlabel:`eq_skip-gram-log`\n",
    "\n",
    "通过微分，我们可以获得其相对于中心词向量$\\mathbf{v}_c$的梯度为\n",
    "\n",
    "$$\\begin{aligned}\\frac{\\partial \\text{log}\\, P(w_o \\mid w_c)}{\\partial \\mathbf{v}_c}&= \\mathbf{u}_o - \\frac{\\sum_{j \\in \\mathcal{V}} \\exp(\\mathbf{u}_j^\\top \\mathbf{v}_c)\\mathbf{u}_j}{\\sum_{i \\in \\mathcal{V}} \\exp(\\mathbf{u}_i^\\top \\mathbf{v}_c)}\\\\&= \\mathbf{u}_o - \\sum_{j \\in \\mathcal{V}} \\left(\\frac{\\text{exp}(\\mathbf{u}_j^\\top \\mathbf{v}_c)}{ \\sum_{i \\in \\mathcal{V}} \\text{exp}(\\mathbf{u}_i^\\top \\mathbf{v}_c)}\\right) \\mathbf{u}_j\\\\&= \\mathbf{u}_o - \\sum_{j \\in \\mathcal{V}} P(w_j \\mid w_c) \\mathbf{u}_j.\\end{aligned}$$\n",
    ":eqlabel:`eq_skip-gram-grad`\n",
    "\n",
    "注意， :eqref:`eq_skip-gram-grad`中的计算需要词典中以$w_c$为中心词的所有词的条件概率。其他词向量的梯度可以以相同的方式获得。\n",
    "\n",
    "对词典中索引为$i$的词进行训练后，得到$\\mathbf{v}_i$（作为中心词）和$\\mathbf{u}_i$（作为上下文词）两个词向量。在自然语言处理应用中，跳元模型的中心词向量通常用作词表示。\n",
    "\n",
    "## 连续词袋（CBOW）模型\n",
    "\n",
    "*连续词袋*（CBOW）模型类似于跳元模型。与跳元模型的主要区别在于，连续词袋模型假设中心词是基于其在文本序列中的周围上下文词生成的。例如，在文本序列“the”“man”“loves”“his”“son”中，在“loves”为中心词且上下文窗口为2的情况下，连续词袋模型考虑基于上下文词“the”“man”“him”“son”（如 :numref:`fig_cbow`所示）生成中心词“loves”的条件概率，即：\n",
    "\n",
    "$$P(\\textrm{\"loves\"}\\mid\\textrm{\"the\"},\\textrm{\"man\"},\\textrm{\"his\"},\\textrm{\"son\"}).$$\n",
    "\n",
    "![连续词袋模型考虑了给定周围上下文词生成中心词条件概率](../img/cbow.svg)\n",
    ":label:`fig_cbow`\n",
    "\n",
    "\n",
    "由于连续词袋模型中存在多个上下文词，因此在计算条件概率时对这些上下文词向量进行平均。具体地说，对于字典中索引$i$的任意词，分别用$\\mathbf{v}_i\\in\\mathbb{R}^d$和$\\mathbf{u}_i\\in\\mathbb{R}^d$表示用作*上下文*词和*中心*词的两个向量（符号与跳元模型中相反）。给定上下文词$w_{o_1}, \\ldots, w_{o_{2m}}$（在词表中索引是$o_1, \\ldots, o_{2m}$）生成任意中心词$w_c$（在词表中索引是$c$）的条件概率可以由以下公式建模:\n",
    "\n",
    "$$P(w_c \\mid w_{o_1}, \\ldots, w_{o_{2m}}) = \\frac{\\text{exp}\\left(\\frac{1}{2m}\\mathbf{u}_c^\\top (\\mathbf{v}_{o_1} + \\ldots, + \\mathbf{v}_{o_{2m}}) \\right)}{ \\sum_{i \\in \\mathcal{V}} \\text{exp}\\left(\\frac{1}{2m}\\mathbf{u}_i^\\top (\\mathbf{v}_{o_1} + \\ldots, + \\mathbf{v}_{o_{2m}}) \\right)}.$$\n",
    ":eqlabel:`fig_cbow-full`\n",
    "\n",
    "为了简洁起见，我们设为$\\mathcal{W}_o= \\{w_{o_1}, \\ldots, w_{o_{2m}}\\}$和$\\bar{\\mathbf{v}}_o = \\left(\\mathbf{v}_{o_1} + \\ldots, + \\mathbf{v}_{o_{2m}} \\right)/(2m)$。那么 :eqref:`fig_cbow-full`可以简化为：\n",
    "\n",
    "$$P(w_c \\mid \\mathcal{W}_o) = \\frac{\\exp\\left(\\mathbf{u}_c^\\top \\bar{\\mathbf{v}}_o\\right)}{\\sum_{i \\in \\mathcal{V}} \\exp\\left(\\mathbf{u}_i^\\top \\bar{\\mathbf{v}}_o\\right)}.$$\n",
    "\n",
    "给定长度为$T$的文本序列，其中时间步$t$处的词表示为$w^{(t)}$。对于上下文窗口$m$，连续词袋模型的似然函数是在给定其上下文词的情况下生成所有中心词的概率：\n",
    "\n",
    "$$ \\prod_{t=1}^{T}  P(w^{(t)} \\mid  w^{(t-m)}, \\ldots, w^{(t-1)}, w^{(t+1)}, \\ldots, w^{(t+m)}).$$\n",
    "\n",
    "### 训练\n",
    "\n",
    "训练连续词袋模型与训练跳元模型几乎是一样的。连续词袋模型的最大似然估计等价于最小化以下损失函数：\n",
    "\n",
    "$$  -\\sum_{t=1}^T  \\text{log}\\, P(w^{(t)} \\mid  w^{(t-m)}, \\ldots, w^{(t-1)}, w^{(t+1)}, \\ldots, w^{(t+m)}).$$\n",
    "\n",
    "请注意，\n",
    "\n",
    "$$\\log\\,P(w_c \\mid \\mathcal{W}_o) = \\mathbf{u}_c^\\top \\bar{\\mathbf{v}}_o - \\log\\,\\left(\\sum_{i \\in \\mathcal{V}} \\exp\\left(\\mathbf{u}_i^\\top \\bar{\\mathbf{v}}_o\\right)\\right).$$\n",
    "\n",
    "通过微分，我们可以获得其关于任意上下文词向量$\\mathbf{v}_{o_i}$（$i = 1, \\ldots, 2m$）的梯度，如下：\n",
    "\n",
    "$$\\frac{\\partial \\log\\, P(w_c \\mid \\mathcal{W}_o)}{\\partial \\mathbf{v}_{o_i}} = \\frac{1}{2m} \\left(\\mathbf{u}_c - \\sum_{j \\in \\mathcal{V}} \\frac{\\exp(\\mathbf{u}_j^\\top \\bar{\\mathbf{v}}_o)\\mathbf{u}_j}{ \\sum_{i \\in \\mathcal{V}} \\text{exp}(\\mathbf{u}_i^\\top \\bar{\\mathbf{v}}_o)} \\right) = \\frac{1}{2m}\\left(\\mathbf{u}_c - \\sum_{j \\in \\mathcal{V}} P(w_j \\mid \\mathcal{W}_o) \\mathbf{u}_j \\right).$$\n",
    ":eqlabel:`eq_cbow-gradient`\n",
    "\n",
    "其他词向量的梯度可以以相同的方式获得。与跳元模型不同，连续词袋模型通常使用上下文词向量作为词表示。\n",
    "\n",
    "## 小结\n",
    "\n",
    "* 词向量是用于表示单词意义的向量，也可以看作词的特征向量。将词映射到实向量的技术称为词嵌入。\n",
    "* word2vec工具包含跳元模型和连续词袋模型。\n",
    "* 跳元模型假设一个单词可用于在文本序列中，生成其周围的单词；而连续词袋模型假设基于上下文词来生成中心单词。\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. 计算每个梯度的计算复杂度是多少？如果词表很大，会有什么问题呢？\n",
    "1. 英语中的一些固定短语由多个单词组成，例如“new york”。如何训练它们的词向量？提示:查看word2vec论文的第四节 :cite:`Mikolov.Sutskever.Chen.ea.2013`。\n",
    "1. 让我们以跳元模型为例来思考word2vec设计。跳元模型中两个词向量的点积与余弦相似度之间有什么关系？对于语义相似的一对词，为什么它们的词向量（由跳元模型训练）的余弦相似度可能很高？\n",
    "\n",
    "[Discussions](https://discuss.d2l.ai/t/5744)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8c5e68ff-67f3-4c7e-b661-748745e5510a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "ms_d2l",
   "language": "python",
   "name": "ms_d2l"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
